package ignore

import (
	"bufio"
	"io"
	"log"
	"os"
	"path/filepath"
	"strings"
)

type Matcher struct {
	patterns []*pattern
}

func New(r io.Reader, dir string) (matcher *Matcher) {
	matcher = &Matcher{}
	dms := splitPath(dir)
	scanner := bufio.NewScanner(r)
	for scanner.Scan() {
		if s := strings.TrimSpace(scanner.Text()); len(s) > 0 && s[0] != '#' {
			matcher.patterns = append(matcher.patterns, parsePattern(s, dms))
		}
	}
	return
}

func FromFile(ignoreFn string, dir string) (matcher *Matcher, err error) {
	ignoreFn = filepath.FromSlash(ignoreFn)
	var f *os.File
	if f, err = os.Open(ignoreFn); err == nil {
		defer closeCloser(f)
		if dir == "" {
			dir = filepath.Dir(ignoreFn)
		}
		matcher = New(f, dir)
	}
	return
}

func (matcher *Matcher) Match(path string, isDir bool) bool {
	paths := splitPath(path)
	if len(paths) == 0 {
		return false
	}

	n := len(matcher.patterns)
	if len(matcher.patterns) == 0 {
		return false
	}
	for i := n - 1; i >= 0; i-- {
		if match := matcher.patterns[i].match(paths, isDir); match > noMatch {
			return match == exclude
		}
	}
	return false
}

// matchResult defines outcomes of a match, no match, exclusion or inclusion.
type matchResult int

const (
	noMatch matchResult = iota //defines the no match outcome of a match check
	exclude                    //defines an exclusion of a file as a result of a match check
	include                    //defines an explicit inclusion of a file as a result of a match check

	inclusionPrefix = "!"
	zeroToManyDirs  = "**"
	patternDirSep   = "/"
)

type pattern struct {
	domain    []string
	pattern   []string
	inclusion bool
	dirOnly   bool
	isGlob    bool
}

// parsePattern parses a gitignore pattern string into the pattern structure.
func parsePattern(p string, domain []string) *pattern {
	res := pattern{domain: domain}

	if strings.HasPrefix(p, inclusionPrefix) {
		res.inclusion = true
		p = p[1:]
	}

	if !strings.HasSuffix(p, "\\ ") {
		p = strings.TrimRight(p, " ")
	}

	if strings.HasSuffix(p, patternDirSep) {
		res.dirOnly = true
		p = p[:len(p)-1]
	}

	if strings.Contains(p, patternDirSep) {
		res.isGlob = true
	}

	res.pattern = strings.Split(p, patternDirSep)
	return &res
}

func (p *pattern) match(path []string, isDir bool) matchResult {
	if len(path) <= len(p.domain) {
		return noMatch
	}
	for i, e := range p.domain {
		if path[i] != e {
			return noMatch
		}
	}

	path = path[len(p.domain):]
	if p.isGlob && !p.globMatch(path, isDir) {
		return noMatch
	} else if !p.isGlob && !p.simpleNameMatch(path, isDir) {
		return noMatch
	}

	if p.inclusion {
		return include
	} else {
		return exclude
	}
}

func (p *pattern) simpleNameMatch(path []string, isDir bool) bool {
	for i, name := range path {
		if match, err := filepath.Match(p.pattern[0], name); err != nil {
			return false
		} else if !match {
			continue
		}
		if p.dirOnly && !isDir && i == len(path)-1 {
			return false
		}
		return true
	}
	return false
}

func (p *pattern) globMatch(path []string, isDir bool) bool {
	matched := false
	canTraverse := false
	for i, pattern := range p.pattern {
		if pattern == "" {
			canTraverse = false
			continue
		}
		if pattern == zeroToManyDirs {
			if i == len(p.pattern)-1 {
				break
			}
			canTraverse = true
			continue
		}
		if strings.Contains(pattern, zeroToManyDirs) {
			return false
		}
		if len(path) == 0 {
			return false
		}
		if canTraverse {
			canTraverse = false
			for len(path) > 0 {
				e := path[0]
				path = path[1:]
				if match, err := filepath.Match(pattern, e); err != nil {
					return false
				} else if match {
					matched = true
					break
				} else if len(path) == 0 {
					// if nothing left then fail
					matched = false
				}
			}
		} else {
			if match, err := filepath.Match(pattern, path[0]); err != nil || !match {
				return false
			}
			matched = true
			path = path[1:]
		}
	}
	if matched && p.dirOnly && !isDir && len(path) == 0 {
		matched = false
	}
	return matched
}

func splitPath(path string) []string {
	return splitString(filepath.ToSlash(path), "/")
}

func splitString(s string, sep string) (ss []string) {
	if s != "" {
		ss = strings.Split(s, sep)
	}
	return
}

func closeCloser(closer io.Closer) {
	if closer != nil {
		if err := closer.Close(); err != nil {
			log.Printf("close %T error: %v", closer, err)
		}
	}
}
